/**
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package org.python.pydev.core.path_watch;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import name.pachler.nio.file.ClosedWatchServiceException;
import name.pachler.nio.file.FileSystems;
import name.pachler.nio.file.Path;
import name.pachler.nio.file.Paths;
import name.pachler.nio.file.StandardWatchEventKind;
import name.pachler.nio.file.WatchEvent;
import name.pachler.nio.file.WatchEvent.Kind;
import name.pachler.nio.file.WatchKey;
import name.pachler.nio.file.WatchService;
import name.pachler.nio.file.ext.ExtendedWatchEventKind;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.python.pydev.core.ListenerList;
import org.python.pydev.core.log.Log;
import com.aptana.shared_core.io.FileUtils;
import com.aptana.shared_core.string.FastStringBuffer;
/**
* @author fabioz
*
* Service to watch filesystem changes at a given path. Works with JPathWatch.
*
* Multiple events are stacked and reported from time to time.
*
* When a key that is tracked is removed from the filesystem, it enters in a poll job (invalidPathsRestorer)
* which will notify when it's recreated (note that it's only (re)scheduled if there is some available invalid path).
*/
public class PathWatch {
/**
* The service that'll give us notifications.
*/
private WatchService watchService;
/**
* If != null, logs will be added to this buffer.
*/
public static FastStringBuffer log;
/**
* The path being watched and the stacker object that'll stack many requests into one.
*
* The stacker object contains the actual key in the watchService (although it may be none if the key
* becomes invalid).
*/
private Map<Path, EventsStackerRunnable> pathToStacker = new HashMap<Path, EventsStackerRunnable>();
private final Object keyToPathLock = new Object();
private Map<WatchKey, Path> keyToPath = new HashMap<WatchKey, Path>();
private final Object invalidPathsLock = new Object();
private volatile Set<EventsStackerRunnable> invalidPaths = new HashSet<EventsStackerRunnable>();
/*default*/Set<EventsStackerRunnable> getInvalidPaths() {
synchronized (invalidPathsLock) {
//Always return a copy!
return new HashSet<EventsStackerRunnable>(invalidPaths);
}
}
private final PollThread pollThread;
private final Object lock = new Object();
private volatile List<Runnable> runnables = new ArrayList<Runnable>();
private final Job jobRunRunnables = new Job("PathWatch notifier") {
@Override
protected IStatus run(IProgressMonitor monitor) {
//Clients will actually be notified in this job.
List<Runnable> curr = runnables;
runnables = new ArrayList<Runnable>();
for (Runnable runnable : curr) {
try {
runnable.run();
} catch (Exception e) {
Log.log(e);
}
}
return Status.OK_STATUS;
}
};
public static int RECHECK_INVALID_PATHS_EACH = 4000;
private final Job invalidPathsRestorer = new Job("Invalid paths restorer") {
@Override
protected IStatus run(IProgressMonitor monitor) {
synchronized (invalidPathsLock) {
if (log != null) {
log.append('.');
}
Set<EventsStackerRunnable> remove = new HashSet<EventsStackerRunnable>();
for (Iterator<EventsStackerRunnable> it = invalidPaths.iterator(); it.hasNext();) {
EventsStackerRunnable r = it.next();
IFilesystemChangesListener[] listeners = r.list.getListeners();
if (listeners.length == 0) {
if (log != null) {
log.append("Removing stacker from invalid list (because it has no listeners): ")
.appendObject(r).append('\n');
}
remove.add(r); //remove last iterated (no longer watched)
} else {
File f = new File(r.watchedPath.toString());
if (f.exists()) {
for (IFilesystemChangesListener listener : listeners) {
listener.added(f);
}
try {
WatchKey key = r.watchedPath.register(watchService,
StandardWatchEventKind.ENTRY_CREATE, StandardWatchEventKind.ENTRY_DELETE,
StandardWatchEventKind.ENTRY_MODIFY, StandardWatchEventKind.OVERFLOW,
ExtendedWatchEventKind.KEY_INVALID);
//only add to be removed if it was successful...
r.key = key;
synchronized (keyToPathLock) {
keyToPath.put(key, r.watchedPath);
}
if (log != null) {
log.append("Removing stacker from invalid list because it became valid again: ")
.appendObject(r).append('\n');
}
remove.add(r); //remove last iterated (valid again)
} catch (UnsupportedOperationException uox) {
Log.log(uox);
} catch (IOException iox) {
//Ignore: it may not exist now, but may start existing later on...
if (log != null) {
log.append("IOException when trying to make valid: " + r.watchedPath);
}
} catch (Throwable e) {
Log.log(e);
}
}
}
}
invalidPaths.removeAll(remove);
//Re-add the ones not removed...
int size = invalidPaths.size();
if (log != null) {
if (size < 0) {
//This could happen when access to invalidPaths is not properly synched with invalidPathsLock!
log.append("\nBUG BUG BUG: Size: ").append(size).append('\n');
}
}
if (size > 0) {
this.schedule(RECHECK_INVALID_PATHS_EACH);
if (log != null) {
log.append("!");
}
} else {
if (log != null) {
log.append("NOT rescheduling; size=").append(size).append(";invalidPaths=")
.appendObject(invalidPaths).append('\n');
}
}
}
return Status.OK_STATUS;
}
};
/**
* After receiving a change, it'll only be notified after this time elapses (in millis).
* This means that while we have a change it may be that what's reported actually changes
* (i.e.: if a file is added and removed, only the removal will be recorded).
*/
public static int TIME_BEFORE_NOTIFY = 250;
private PathWatch() {
watchService = FileSystems.getDefault().newWatchService();
pollThread = new PollThread();
pollThread.start();
}
private class PollThread extends Thread {
public void run() {
for (;;) {
// take() will block until a file has been created/deleted
WatchKey signalledKey;
try {
signalledKey = watchService.take();
} catch (InterruptedException ix) {
// we'll ignore being interrupted
if (log != null) {
log.append("Interrupted\n");
}
continue;
} catch (ClosedWatchServiceException cwse) {
// other thread closed watch service
System.out.println("watch service closed, terminating.");
break;
}
List<WatchEvent<?>> list;
Path watchedPath;
EventsStackerRunnable stacker;
synchronized (lock) {
synchronized (keyToPathLock) {
watchedPath = keyToPath.get(signalledKey);
}
if (watchedPath == null) {
continue;
}
// get list of events from key
list = signalledKey.pollEvents();
stacker = pathToStacker.get(watchedPath);
if (stacker == null) {
//if the stacker does not exist, go on without rescheduling the key!
if (log != null) {
log.append("Stacker for: ").appendObject(watchedPath).append("is null\n");
}
continue;
}
runnables.add(stacker);
for (WatchEvent<?> e : list) {
Path context = (Path) e.context();
Path resolve = watchedPath.resolve(context);
File file = new File(resolve.toString());
Kind<?> kind = e.kind();
if (log != null) {
log.append("Event: ").appendObject(e).append('\n');
}
if (kind == StandardWatchEventKind.OVERFLOW) {
if (!file.exists()) {
//It may be that it became invalid...
synchronized (keyToPathLock) {
keyToPath.remove(signalledKey);
}
stacker.key = null;
addInvalidPath(stacker);
stacker.removed(file);
} else {
// VERY IMPORTANT! call reset() AFTER pollEvents() to allow the
// key to be reported again by the watch service.
signalledKey.reset();
if (log != null) {
log.append("Key reset to hear changes");
}
}
//On an overflow, wait a bit and signal that all files being watched were removed,
//do a list and say that the current files were added again.
stacker.overflow(file);
} else {
if (kind == StandardWatchEventKind.ENTRY_CREATE
|| kind == StandardWatchEventKind.ENTRY_MODIFY) {
// VERY IMPORTANT! call reset() AFTER pollEvents() to allow the
// key to be reported again by the watch service.
signalledKey.reset();
if (log != null) {
log.append("Key reset to hear changes");
}
stacker.added(file);
} else if (kind == StandardWatchEventKind.ENTRY_DELETE) {
// VERY IMPORTANT! call reset() AFTER pollEvents() to allow the
// key to be reported again by the watch service.
signalledKey.reset();
if (log != null) {
log.append("Key reset to hear changes");
}
stacker.removed(file);
} else if (kind == ExtendedWatchEventKind.KEY_INVALID) {
//Invalidated means it was removed... (so, no need to reschedule to listen again)
synchronized (keyToPathLock) {
keyToPath.remove(signalledKey);
}
stacker.key = null;
addInvalidPath(stacker);
stacker.removed(file);
}
}
}
}
if (runnables.size() > 0) {
jobRunRunnables.schedule(TIME_BEFORE_NOTIFY);
}
}
}
}
private static PathWatch singleton = null;
public static PathWatch get() {
if (singleton == null) {
singleton = new PathWatch();
}
return singleton;
}
public void stopTrack(File path, IFilesystemChangesListener listener) {
Assert.isNotNull(path);
Assert.isNotNull(listener);
Path watchedPath = Paths.get(FileUtils.getFileAbsolutePath(path));
if (log != null) {
log.append("STOP Track: ").appendObject(path).append("Listener: ").appendObject(listener).append('\n');
}
synchronized (lock) {
EventsStackerRunnable stacker = pathToStacker.get(watchedPath);
if (stacker != null && stacker.list != null) {
ListenerList<IFilesystemChangesListener> list = stacker.list;
list.remove(listener);
if (list.getListeners().length == 0) {
pathToStacker.remove(watchedPath);
synchronized (keyToPathLock) {
keyToPath.remove(stacker.key);
}
synchronized (invalidPathsLock) {
if (log != null) {
log.append("Remove from invalid paths (no listeners): ").appendObject(stacker).append('\n');
}
invalidPaths.remove(stacker);
}
}
}
}
}
/**
* A listener will start tracking changes at the given path.
*/
public void track(File path, IFilesystemChangesListener listener) {
Assert.isNotNull(path);
Assert.isNotNull(listener);
Path watchedPath = Paths.get(FileUtils.getFileAbsolutePath(path));
synchronized (lock) {
EventsStackerRunnable stacker = pathToStacker.get(watchedPath);
if (stacker != null) {
//already being tracked -- or already in invalid list ;)
stacker.list.add(listener);
return;
}
if (log != null) {
log.append("Track: ").appendObject(path).append("Listener: ").appendObject(listener).append('\n');
}
boolean add = true;
WatchKey key = null;
try {
key = watchedPath.register(watchService, StandardWatchEventKind.ENTRY_CREATE,
StandardWatchEventKind.ENTRY_DELETE, StandardWatchEventKind.ENTRY_MODIFY,
StandardWatchEventKind.OVERFLOW, ExtendedWatchEventKind.KEY_INVALID);
} catch (UnsupportedOperationException uox) {
if (log != null) {
log.append("UnsupportedOperationException: ").appendObject(uox).append('\n');
}
add = false;
Log.log(uox);
} catch (IOException iox) {
//Ignore: it may not exist now, but may start existing later on...
} catch (Throwable e) {
if (log != null) {
log.append("Throwable: ").appendObject(e).append('\n');
}
add = false;
Log.log(e);
}
if (add) {
if (stacker == null) {
stacker = new EventsStackerRunnable(key, watchedPath, new ListenerList<IFilesystemChangesListener>(
IFilesystemChangesListener.class));
pathToStacker.put(watchedPath, stacker);
}
stacker.list.add(listener);
if (key != null) {
synchronized (keyToPathLock) {
keyToPath.put(key, watchedPath);
}
} else {
//Will go to our poll service to start tracking when it becomes valid...
addInvalidPath(stacker);
}
}
}
}
private void addInvalidPath(EventsStackerRunnable stacker) {
if (log != null) {
log.append("addInvalidPath: ").appendObject(stacker).append('\n');
}
synchronized (invalidPathsLock) {
invalidPaths.add(stacker);
}
invalidPathsRestorer.schedule(RECHECK_INVALID_PATHS_EACH);
}
}